經過前面 27 天的努力,我們的 AI 個人財務追蹤器已經順利上雲,並裝上了 Sentry 與 Logtail 防禦天眼。然而,隨著專案功能越疊越多,昨天在 Sentry 儀表板上出現的幾筆零星報錯,暴露出了一個所有 Vibe Coder 都無法迴避的硬傷——技術債(Technical Debt)爆發。
在 Vibe Coding 模式下,我們習慣用自然語言快速下 Prompt 讓 AI 生成程式碼。AI 為了最快達成「功能可執行」的目標,常常會大量複製貼上類似的 API 請求、將 State 混亂散落於數個組件間、或是寫出長達 500 行的「巨石型組件(God Component)」。
今天我們的目標是對專案進行大刀闊斧的技術債清理與重構:
當你直接對 AI 說「請幫我重構這段程式碼」時,AI 最喜歡發揮「想像力」,自作主張地刪除功能、更換套件甚至重新發明輪子。要讓 AI 成為重構神隊友,我們必須給予明確的設計模式(Design Pattern)與重構目標。
我們選定了專案中最混亂的 FinancialDashboard.tsx,這段程式碼原本包含了 10 個 useState、3 個 useEffect,並在前端手動過濾財務紀錄,導致畫面卡頓且極難維護。
你是一位資深 React & TypeScript 架構師。
我們要對 `FinancialDashboard.tsx` 進行重構,以清除 AI 早期生成程式碼累積的技術債。
重構目標與約束條件:
1. **抽離狀態管理**:將原本散落在元件內的 10 個 `useState` 與資料獲取邏輯,重構為自訂 Hook `useFinancialData`(結合 TanStack Query / SWR)。
2. **單一職責原則 (SRP)**:將原本 400 行的巨石組件,拆分為 `<StatCardsSection />`、`<ExpenseChartSection />` 與 `<TransactionTableSection />` 獨立子組件。
3. **消除 Redundant Rendering**:使用 `useMemo` 與 `useCallback` 優化高頻率 Filter/Sort 計算。
4. **絕對限制**:不可變更任何 UI 外觀與既有功能,必須確保所有型別嚴格對齊,不要引入新的第三方庫。
AI 生成了重構後的 Custom Hook 與組件結構:
// AI 生成的 Custom Hook (hooks/useFinancialData.ts)
import { useState, useEffect } from 'react';
export function useFinancialData() {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
const [filter, setFilter] = useState('all');
useEffect(() => {
fetch(`/api/transactions?filter=${filter}`)
.then((res) => res.json())
.then((data) => {
setData(data);
setLoading(false);
});
}, [filter]);
return { data, loading, filter, setFilter };
}
// AI 生成的重構後主元件 (FinancialDashboard.tsx)
import { useFinancialData } from '@/hooks/useFinancialData';
import { StatCardsSection } from './StatCardsSection';
import { ExpenseChartSection } from './ExpenseChartSection';
export function FinancialDashboard() {
const { data, loading, filter, setFilter } = useFinancialData();
if (loading) return <div>Loading...</div>;
return (
<div className="space-y-6">
<StatCardsSection data={data} />
<ExpenseChartSection data={data} />
</div>
);
}
AI 的重構乍看之下完成了「檔案拆分」,但一放到真實環境執行,立刻陷入了經典的「越重構越爛」Vibe Coding 泥沼!
專案在 Day 15 時已經引進了 TanStack Query (React Query) 來處理前後端 API 快取與自動 Revalidate。AI 在重構 Hook 時,居然「退步」用回了最原始的 useEffect + fetch!這導致使用者切換 Filter 時,若舊請求比較晚回傳,會產生狀態覆蓋(Race Condition),讓頁面呈現錯誤數據!
AI 雖然建立了 <StatCardsSection/>,但為了傳遞使用者點擊事件,直接把 setFilter、data、refetch 一路往下層傳了四層(Prop Drilling)。更糟的是,AI 忘記用 useCallback 包裹子組件傳遞的 Handler,導致每次父組件狀態一變,所有子圖表與表格全部強制重新渲染!
我們再次下達精確指示,規範 AI 必須基於現代 React Best Practices 重構:
useMemo 完成資料篩選與金額加總,不將計算負擔留給 UI 組件。修正後的重構程式碼:
hooks/useFinancialData.ts(強健的 Custom Hook):
import { useQuery } from '@tanstack/react-query';
import { useMemo, useState, useCallback } from 'react';
import { Transaction } from '@prisma/client';
export function useFinancialData() {
const [selectedCategory, setSelectedCategory] = useState<string>('ALL');
// 1. 保留 React Query 帶來的快取與 Race Condition 防禦
const { data: transactions = [], isLoading, isError, refetch } = useQuery<Transaction[]>({
queryKey: ['transactions'],
queryFn: async () => {
const res = await fetch('/api/transactions');
if (!res.ok) throw new Error('Failed to fetch transactions');
return res.json();
},
staleTime: 1000 * 60 * 5, // 5 分鐘內維持 fresh 狀態
});
// 2. 集中複雜的資料轉換與統計計算 (避免在 UI 組件中重複計算)
const filteredTransactions = useMemo(() => {
if (selectedCategory === 'ALL') return transactions;
return transactions.filter(t => t.category === selectedCategory);
}, [transactions, selectedCategory]);
const summary = useMemo(() => {
return filteredTransactions.reduce(
(acc, t) => {
if (t.type === 'INCOME') acc.totalIncome += t.amount;
if (t.type === 'EXPENSE') acc.totalExpense += t.amount;
return acc;
},
{ totalIncome: 0, totalExpense: 0 }
);
}, [filteredTransactions]);
const handleCategoryChange = useCallback((category: string) => {
setSelectedCategory(category);
}, []);
return {
transactions: filteredTransactions,
summary,
isLoading,
isError,
selectedCategory,
handleCategoryChange,
refetch,
};
}
components/dashboard/FinancialDashboard.tsx(乾淨俐落的主組件):
'use client';
import { useFinancialData } from '@/hooks/useFinancialData';
import { StatCardsSection } from './StatCardsSection';
import { ExpenseChartSection } from './ExpenseChartSection';
import { Button } from '@/components/ui/button';
import { RefreshCw } from 'lucide-react';
export function FinancialDashboard() {
const {
transactions,
summary,
isLoading,
isError,
selectedCategory,
handleCategoryChange,
refetch,
} = useFinancialData();
if (isLoading) return <DashboardSkeleton />;
if (isError) return <DashboardErrorState onRetry={refetch} />;
return (
<div className="space-y-8 p-6 max-w-7xl mx-auto">
<div className="flex justify-between items-center">
<h1 className="text-3xl font-bold tracking-tight">財務儀表板</h1>
<Button variant="outline" size="sm" onClick={() => refetch()}>
<RefreshCw className="h-4 w-4 mr-2" /> 重新整理
</Button>
</div>
{/* 數據卡片區域 */}
<StatCardsSection summary={summary} />
{/* 圖表與動態表格區域 */}
<ExpenseChartSection
transactions={transactions}
currentCategory={selectedCategory}
onSelectCategory={handleCategoryChange}
/>
</div>
);
}
npm run test 全綠過關,這才是真正的「Vibe Mode」安全重構!